import _ from 'lodash';
import logger from './logger';
import {
  processCapabilities,
  STANDARD_CAPS,
  errors,
  isW3cCaps,
} from '@appium/base-driver';
import {inspect as dump} from 'util';
import {node, fs} from '@appium/support';
import path from 'path';
import {SERVER_SUBCOMMAND, DRIVER_TYPE, PLUGIN_TYPE, SETUP_SUBCOMMAND} from './constants';
import os from 'node:os';

const W3C_APPIUM_PREFIX = 'appium';
const STANDARD_CAPS_LOWERCASE = new Set([...STANDARD_CAPS].map((cap) => cap.toLowerCase()));
export const V4_BROADCAST_IP = '0.0.0.0';
export const V6_BROADCAST_IP = '::';
export const npmPackage = fs.readPackageJsonFrom(__dirname);

/**
 *
 * If `stdout` is a TTY, this is `true`.
 *
 * Used for tighter control over log output.
 * @type {boolean}
 */
const isStdoutTTY = process.stdout.isTTY;

/**
 * Creates an error object in case a session gets incompatible capabilities as the input.
 *
 * @returns {Error}
 */
export function makeNonW3cCapsError() {
  return new errors.SessionNotCreatedError(
    'Session capabilities format must comply to the W3C standard. Make sure your client is up to date. ' +
    'See https://www.w3.org/TR/webdriver/#new-session for more details.'
  );
}

/**
 * Dumps to value to the console using `info` logger.
 *
 * @todo May want to force color to be `false` if {@link isStdoutTTY} is `false`.
 */
export const inspect = _.flow(
  _.partialRight(
    /** @type {(object: any, options: import('util').InspectOptions) => string} */ (dump),
    {colors: true, depth: null, compact: !isStdoutTTY}
  ),
  (...args) => {
    logger.info(...args);
  }
);

/**
 * Takes the caps that were provided in the request and translates them
 * into caps that can be used by the inner drivers.
 *
 * @template {Constraints} C
 * @param {W3CCapabilities<C>} w3cCapabilities
 * @param {C} constraints
 * @param {NSCapabilities<C>} [defaultCapabilities]
 * @returns {ParsedDriverCaps<C>|InvalidCaps<C>}
 */
export function parseCapsForInnerDriver(
  w3cCapabilities,
  constraints = /** @type {C} */ ({}),
  defaultCapabilities = {}
) {
  if (!isW3cCaps(w3cCapabilities)) {
    return /** @type {InvalidCaps<C>} */ ({
      error: makeNonW3cCapsError(),
    });
  }

  let desiredCaps = /** @type {ParsedDriverCaps<C>['desiredCaps']} */ ({});
  /** @type {ParsedDriverCaps<C>['processedW3CCapabilities'] | undefined} */
  let processedW3CCapabilities;

  // Make sure we don't mutate the original arguments
  w3cCapabilities = _.cloneDeep(w3cCapabilities);
  defaultCapabilities = _.cloneDeep(defaultCapabilities);

  if (!_.isEmpty(defaultCapabilities)) {
    for (const [defaultCapKey, defaultCapValue] of _.toPairs(defaultCapabilities)) {
      let isCapAlreadySet = false;
      // Check if the key is already present in firstMatch entries
      for (const firstMatchEntry of w3cCapabilities.firstMatch ?? []) {
        if (
          _.isPlainObject(firstMatchEntry) &&
          _.has(removeAppiumPrefixes(firstMatchEntry), removeAppiumPrefix(defaultCapKey))
        ) {
          isCapAlreadySet = true;
          break;
        }
      }
      // Check if the key is already present in alwaysMatch entries
      isCapAlreadySet =
        isCapAlreadySet ||
        (_.isPlainObject(w3cCapabilities.alwaysMatch) &&
          _.has(
            removeAppiumPrefixes(w3cCapabilities.alwaysMatch),
            removeAppiumPrefix(defaultCapKey)
          ));
      if (isCapAlreadySet) {
        // Skip if the key is already present in the provided caps
        continue;
      }

      // Only add the default capability if it is not overridden
      if (_.isEmpty(w3cCapabilities.firstMatch)) {
        w3cCapabilities.firstMatch = /** @type {W3CCapabilities<C>['firstMatch']} */ ([
          {[defaultCapKey]: defaultCapValue},
        ]);
      } else {
        w3cCapabilities.firstMatch[0][defaultCapKey] = defaultCapValue;
      }
    }
  }

  // Call the process capabilities algorithm to find matching caps on the W3C
  // (see: https://github.com/jlipps/simple-wd-spec#processing-capabilities)
  try {
    desiredCaps = processCapabilities(w3cCapabilities, constraints, true);
  } catch (error) {
    logger.info(`Could not parse W3C capabilities: ${error.message}`);
    return /** @type {InvalidCaps<C>} */ ({
      desiredCaps,
      processedW3CCapabilities,
      error,
    });
  }

  // Create a new w3c capabilities payload that contains only the matching caps in `alwaysMatch`
  processedW3CCapabilities = {
    alwaysMatch: {...insertAppiumPrefixes(desiredCaps)},
    firstMatch: [{}],
  };

  return /** @type {ParsedDriverCaps<C>} */ ({
    desiredCaps,
    processedW3CCapabilities,
  });
}

/**
 * Takes a capabilities objects and prefixes capabilities with `appium:`
 * @template {Constraints} [C={}]
 * @param {Capabilities<C>} caps - Desired capabilities object
 * @returns {NSCapabilities<C>}
 */
export function insertAppiumPrefixes(caps) {
  return /** @type {NSCapabilities<C>} */ (
    _.mapKeys(caps, (_, key) =>
      STANDARD_CAPS_LOWERCASE.has(key.toLowerCase()) || key.includes(':')
        ? key
        : `${W3C_APPIUM_PREFIX}:${key}`
    )
  );
}

/**
 * @template {Constraints} [C={}]
 * @param {NSCapabilities<C>} caps
 * @returns {Capabilities<C>}
 */
export function removeAppiumPrefixes(caps) {
  return /** @type {Capabilities<C>} */ (_.mapKeys(caps, (_, key) => removeAppiumPrefix(key)));
}

/**
 * @param {string} key
 * @returns {string}
 */
function removeAppiumPrefix(key) {
  const prefix = `${W3C_APPIUM_PREFIX}:`;
  return _.startsWith(key, prefix) ? key.substring(prefix.length) : key;
}

/**
 *
 * @param {string} pkgName
 * @returns {string|undefined}
 */
export function getPackageVersion(pkgName) {
  const pkgInfo = require(`${pkgName}/package.json`) || {};
  return pkgInfo.version;
}

/**
 * Returns the root directory of the Appium module.
 *
 * @returns {string} - The absolute path to the Appium module root directory.
 * @throws {Error} - If the Appium module root cannot be determined.
 */
export const getAppiumModuleRoot = _.memoize(function getAppiumModuleRoot() {
  const selfRoot = node.getModuleRootSync('appium', __filename);
  if (!selfRoot) {
    throw new Error('Cannot find the appium module root. This is likely a bug in Appium.');
  }
  return selfRoot;
});

/**
 * Adjusts NODE_PATH environment variable,
 * so CJS drivers and plugins could load their peer dependencies.
 * Read https://nodejs.org/api/modules.html#loading-from-the-global-folders
 * for more details.
 *
 * Unfortunately this hack does not work with ESM modules,
 * @returns {void}
 */
export function adjustNodePath() {
  let appiumModuleSearchRoot;
  try {
    appiumModuleSearchRoot = path.dirname(getAppiumModuleRoot());
  } catch (error) {
    logger.warn(error.message);
    return;
  }

  const refreshRequirePaths = () => {
    try {
      // ! This hack allows us to avoid modification of import
      // ! statements in client modules. It uses a private API though,
      // ! so it could break (maybe, eventually).
      // See https://gist.github.com/branneman/8048520#7-the-hack
      // @ts-ignore see above comment

      require('module').Module._initPaths();
      return true;
    } catch {
      return false;
    }
  };

  if (!process.env.NODE_PATH) {
    process.env.NODE_PATH = appiumModuleSearchRoot;
    if (refreshRequirePaths()) {
      process.env.APPIUM_OMIT_PEER_DEPS = '1';
    } else {
      delete process.env.NODE_PATH;
    }
    return;
  }

  const nodePathParts = process.env.NODE_PATH.split(path.delimiter);
  if (nodePathParts.includes(appiumModuleSearchRoot)) {
    process.env.APPIUM_OMIT_PEER_DEPS = '1';
    return;
  }

  nodePathParts.push(appiumModuleSearchRoot);
  process.env.NODE_PATH = nodePathParts.join(path.delimiter);
  if (refreshRequirePaths()) {
    process.env.APPIUM_OMIT_PEER_DEPS = '1';
  } else {
    process.env.NODE_PATH = _.without(nodePathParts, appiumModuleSearchRoot).join(path.delimiter);
  }
}

/**
 * Pulls the initial values of Appium settings from the given capabilities argument.
 * Each setting item must satisfy the following format:
 * `settings[setting_name]: setting_value`
 * or
 * ```
 * settings = {
 *   setting_name1: 'setting_value1',
 *   setting_name2: 'setting_value2',
 * }
 * ```
 * The capabilities argument itself gets mutated, so it does not contain parsed
 * settings anymore to avoid further parsing issues.
 * Check
 * https://appium.io/docs/en/latest/guides/settings/
 * for more details on the available settings.
 *
 * @param {?Object} caps - Capabilities dictionary. It is mutated if
 * one or more settings have been pulled from it
 * @return {Object} - An empty dictionary if the given caps contains no
 * setting items or a dictionary containing parsed Appium setting names along with
 * their values.
 */
export function pullSettings(caps) {
  if (!_.isPlainObject(caps) || _.isEmpty(caps)) {
    return {};
  }

  const result = {};
  const singleSettings = {};
  for (const [key, value] of _.toPairs(caps)) {
    let match;
    if (/^(s|appium:s)ettings$/.test(key) && _.isPlainObject(value)) {
      Object.assign(result, value);
      delete caps[key];
    } else if ((match = /^(s|appium:s)ettings\[(\S+)\]$/.exec(key))) {
      singleSettings[match[2]] = value;
      delete caps[key];
    }
  }
  if (!_.isEmpty(singleSettings)) {
    Object.assign(result, singleSettings);
  }
  return result;
}

/**
 * @template {CliCommand} [Cmd=ServerCommand]
 * @template {CliExtensionSubcommand|void} [SubCmd=void]
 * @param {Args<Cmd, SubCmd>} args
 * @returns {args is Args<ServerCommand>}
 */
export function isServerCommandArgs(args) {
  return args.subcommand === SERVER_SUBCOMMAND;
}

/**
 * @template {CliCommand} Cmd
 * @template {CliExtensionSubcommand|CliCommandSetupSubcommand|void} [SubCmd=void]
 * @param {Args<Cmd, SubCmd>} args
 * @returns {args is Args<SetupCommand>}
 */
export function isSetupCommandArgs(args) {
  return args.subcommand === SETUP_SUBCOMMAND;
}

/**
 * @template {CliCommand} [Cmd=ServerCommand]
 * @template {CliExtensionSubcommand|void} [SubCmd=void]
 * @param {Args<Cmd, SubCmd>} args
 * @returns {args is Args<CliExtensionCommand, SubCmd>}
 */
export function isExtensionCommandArgs(args) {
  return args.subcommand === DRIVER_TYPE || args.subcommand === PLUGIN_TYPE;
}

/**
 * @template {CliCommand} Cmd
 * @template {CliExtensionSubcommand} SubCmd
 * @param {Args<Cmd, SubCmd>} args
 * @returns {args is Args<DriverCommand, SubCmd>}
 */
export function isDriverCommandArgs(args) {
  return args.subcommand === DRIVER_TYPE;
}

/**
 * @template {CliCommand} Cmd
 * @template {CliExtensionSubcommand} SubCmd
 * @param {Args<Cmd, SubCmd>} args
 * @returns {args is Args<PluginCommand, SubCmd>}
 */
export function isPluginCommandArgs(args) {
  return args.subcommand === PLUGIN_TYPE;
}

/**
 * Fetches the list of matched network interfaces of the current host.
 *
 * @param {4|6|null} family Either 4 to include ipv4 addresses only,
 * 6 to include ipv6 addresses only, or null to include all of them
 * @returns {os.NetworkInterfaceInfo[]} The list of matched interfaces
 */
export function fetchInterfaces (family = null) {
  let familyValue = null;
  // 'IPv4' is in Node <= 17, from 18 it's a number 4 or 6
  if (family === 4) {
    familyValue = [4, 'IPv4'];
  } else if (family === 6) {
    familyValue = [6, 'IPv6'];
  }
  // @ts-ignore The linter does not understand the below filter
  return _.flatMap(_.values(os.networkInterfaces()).filter(Boolean))
    // @ts-ignore The linter does not understand the above filter
    .filter(({family}) => !familyValue || familyValue && familyValue.includes(family));
}

/**
 * https://github.com/SheetJS/js-adler32
 *
 * @param {string} str
 * @param {number?} [seed]
 * @returns {number}
 */
export function adler32(str, seed = null) {
	let a = 1, b = 0, L = str.length, M = 0, c = 0, d = 0;
	if (typeof seed === 'number') {
    a = seed & 0xFFFF;
    b = seed >>> 16;
  }
	for (let i = 0; i < L;) {
		M = Math.min(L - i, 2918);
		while (M > 0) {
			c = str.charCodeAt(i++);
			if (c < 0x80) {
        a += c;
      } else if (c < 0x800) {
				a += 192 | ((c >> 6) & 31);
        b += a;
        --M;
				a += 128 | (c & 63);
			} else if (c >= 0xD800 && c < 0xE000) {
				c = (c & 1023) + 64;
        d = str.charCodeAt(i++) & 1023;
				a += 240 | ((c >> 8) & 7);
        b += a;
        --M;
				a += 128 | ((c >> 2) & 63);
        b += a;
        --M;
				a += 128 | ((d >> 6) & 15) | ((c & 3) << 4);
        b += a;
        --M;
				a += 128 | (d & 63);
			} else {
				a += 224 | ((c >> 12) & 15);
        b += a;
        --M;
				a += 128 | ((c >> 6) & 63);
        b += a;
        --M;
				a += 128 | (c & 63);
			}
			b += a;
      --M;
		}
		a = (15 * (a >>> 16) + (a & 65535));
		b = (15 * (b >>> 16) + (b & 65535));
	}
	return ((b % 65521) << 16) | (a % 65521);
}

/**
 * Checks if the provided address is a broadcast one.
 *
 * @param {string} address
 * @returns {boolean}
 */
export function isBroadcastIp(address) {
  return [V4_BROADCAST_IP, V6_BROADCAST_IP, `[${V6_BROADCAST_IP}]`].includes(address);
}

/**
 * @typedef {import('@appium/types').StringRecord} StringRecord
 * @typedef {import('@appium/types').BaseDriverCapConstraints} BaseDriverCapConstraints
 */

/**
 * @template {Constraints} [C=BaseDriverCapConstraints]
 * @typedef ParsedDriverCaps
 * @property {Capabilities<C>} desiredCaps
 * @property {W3CCapabilities<C>} processedW3CCapabilities
 */

/**
 * @todo protocol is more specific
 * @template {Constraints} [C=BaseDriverCapConstraints]
 * @typedef InvalidCaps
 * @property {Error} error
 * @property {Capabilities<C>} [desiredCaps]
 * @property {W3CCapabilities<C>} [processedW3CCapabilities]
 */

/**
 * @template {Constraints} C
 * @typedef {import('@appium/types').Capabilities<C>} Capabilities
 */

/**
 * @template {Constraints} C
 * @typedef {import('@appium/types').W3CCapabilities<C>} W3CCapabilities
 */

/**
 * @template {Constraints} C
 * @typedef {import('@appium/types').NSCapabilities<C>} NSCapabilities
 */

/**
 * @template {Constraints} C
 * @typedef {import('@appium/types').ConstraintsToCaps<C>} ConstraintsToCaps
 */

/**
 * @typedef {import('@appium/types').Constraints} Constraints
 */

/**
 * @typedef {import('appium/types').CliCommand} CliCommand
 * @typedef {import('appium/types').CliExtensionSubcommand} CliExtensionSubcommand
 * @typedef {import('appium/types').CliExtensionCommand} CliExtensionCommand
 * @typedef {import('appium/types').CliCommandSetupSubcommand} CliCommandSetupSubcommand
 * @typedef {import('appium/types').CliCommandServer} ServerCommand
 * @typedef {import('appium/types').CliCommandDriver} DriverCommand
 * @typedef {import('appium/types').CliCommandPlugin} PluginCommand
 * @typedef {import('appium/types').CliCommandSetup} SetupCommand
 */

/**
 * @template {CliCommand} [Cmd=ServerCommand]
 * @template {CliExtensionSubcommand|CliCommandSetupSubcommand|void} [SubCmd=void]
 * @typedef {import('appium/types').Args<Cmd, SubCmd>} Args
 */

/**
 * @template {CliCommand} [Cmd=ServerCommand]
 * @template {CliExtensionSubcommand|CliCommandSetupSubcommand|void} [SubCmd=void]
 * @typedef {import('appium/types').ParsedArgs<Cmd, SubCmd>} ParsedArgs
 */
